05_BDV/03 -- Diseño del esquema en pgvector.md

Diseño del esquema en pgvector

La transición del almacenamiento basado en archivos (FAISS) a un sistema relacional exigió un diseño de base de datos que garantizara la integridad referencial con el resto de módulos de DWall y permitiera una gestión eficiente de los vectores.

Estructura de las tablas de embeddings

La estructura de las tablas donde se almacenan los vectores es, paradójicamente, uno de los aspectos más sencillos del módulo. Aunque cada tipo de recurso de DWall (variables, archivos, consultas, reglas, etc.) cuenta con su propia tabla física (ej. module_embeddings_variable), todas ellas comparten una estructura idéntica:

CampoTipoDescripción
idbigserialClave primaria autoincremental del registro de embedding.
entity_idbigintID del recurso en su módulo de origen (clave foránea lógica).
embedding_texttextEl texto exacto (Markdown) enviado al LLM para generar el vector.
embeddingvector(768)El vector de 768 dimensiones generado por Gemini.
updated_attimestamptzFecha de última actualización para control de frescura.

Esto se traduce en la existencia de 8 tablas diferentes con la misma estructura. Si comparamos este diseño con el del módulo de descripciones, surge una pregunta arquitectónica inmediata: ¿por qué allí se optó por una única tabla centralizada para todas las descripciones (module_description_description) diferenciada por un entity_type, mientras que aquí se han dividido en tablas por recurso?

La respuesta reside en la complejidad de los datos necesarios para cada módulo y en cómo se gestiona la consistencia entre contextos:

1. La necesidad de datos enriquecidos

Para que una descripción sea útil, solo necesitamos el texto y el nombre del recurso asociado. Sin embargo, para generar un embedding con valor semántico real, necesitamos extraer todas las características del recurso que aporten significado.

Por ejemplo, al generar el embedding de una Consulta (Query), no basta con su nombre. Es fundamental incluir la fórmula de la consulta. Si Gemini tiene acceso a la lógica interna de la consulta, el vector resultante capturará mucho mejor su propósito. Lo mismo ocurre con las reglas de negocio o las variables agregadas: su significado reside en su definición técnica tanto como en su nombre.

2. Eventos de Dominio: la regla de la información mínima

En DDD, un Evento de Dominio es una notificación de que algo ha sucedido (ej. VariableChanged). Una regla fundamental de estos eventos es que deben ser ligeros: deben contener el identificador del recurso y, como mucho, la información mínima indispensable (como el nombre).

Llenar un evento con todos los campos de un recurso (fórmulas, configuraciones, estados) es una mala práctica conocida como Fat Events. Esto acoplaría innecesariamente el módulo emisor con los consumidores y sobrecargaría el bus de eventos.

3. El dilema del enriquecimiento: Pureza vs. Viabilidad

Llegados a este punto, nos enfrentamos a un conflicto arquitectónico: si los eventos de dominio deben ser ligeros (ID + Nombre) y el módulo de embeddings necesita información profunda (fórmulas, paths, metadatos técnicos), ¿cómo obtenemos esos datos sin romper el aislamiento entre módulos?

En un diseño DDD purista, se podría optar por proyecciones materializadas que cada módulo origen alimentara, o por consultas síncronas entre servicios. Sin embargo, dadas las limitaciones de tiempo y recursos, se ha optado por una solución que, aunque pueda considerarse "impura" desde la ortodoxia de DDD, resulta extraordinariamente limpia y efectiva: las Vistas Lógicas (No materializadas).

Este diseño delega la "impureza" de los JOINs entre diferentes esquemas de base de datos a las migraciones SQL y a la capa de jOOQ. El módulo de embeddings en sí no "sabe" nada sobre la estructura interna de las variables o las reglas; simplemente consume sus propias vistas:

  • module_embeddings_variable_context
  • module_embeddings_query_context
  • module_embeddings_rule_context

Estas vistas actúan como una Anti-Corruption Layer (ACL) a nivel de base de datos. Absorben la complejidad de las relaciones externas y exponen al módulo de embeddings únicamente los datos mínimos necesarios con un contrato estable. De esta forma, mantenemos la pureza del código Java y la modularidad de los generadores, mientras garantizamos que el "cerebro" del RAG disponga de la máxima profundidad semántica posible para generar sus vectores.

Vistas de Lectura (Read Model)

Siguiendo los principios de CQRS (cap. 4 — CQRS y Proyecciones), el diseño separa el modelo de escritura del modelo de lectura. Las tablas module_embeddings_* son el almacén — están optimizadas para inserción y actualización rápida del vector. Las vistas, en cambio, son el Read Model, y conviene distinguir dos grupos con propósitos distintos:

Vistas de contexto por tipo (module_embeddings_<tipo>_context). Las ocho vistas presentadas en la sección anterior. Cada una hace los JOINs con las tablas externas necesarias y expone el contexto enriquecido del recurso (nombre, descripción, fórmula, unidades, path…). Las consume el pipeline durante la generación del embedding — son la fuente del texto que se envía a Gemini, no del vector resultante.

Vista unificada (module_embeddings_unified). Una vista distinta, pensada para el flujo de búsqueda: realiza un UNION ALL de las ocho tablas de embeddings, homogeneizando sus campos e incluyendo un discriminador entity_type.

Esta vista cumple una función crítica en la robustez del sistema RAG:

  1. Búsqueda Abstracta: Si la consulta del usuario es ambigua o si el orquestador no logra identificar a qué tipo de entidad se refiere la pregunta, el sistema no se detiene. Realiza una búsqueda semántica de forma totalmente abstracta sobre esta vista unificada.
  2. Fallback de Seguridad: Actúa como una red de seguridad. Si el agente "se confunde" y no filtra por tipo de recurso, la vista unificada le permite encontrar la información relevante sin importar en qué tabla física resida el vector.
  3. Descubrimiento Cross-Dominio: Permite encontrar relaciones entre diferentes tipos de recursos (ej. una variable y una regla que la menciona) en una sola operación de base de datos.

Resumen del modelo

El diagrama siguiente muestra el esquema completo para una de las ocho entidades — la Variable — incluyendo las tablas externas que alimentan a la vista de contexto y la vista unificada cross-type. El mismo patrón se replica de forma idéntica para Rule, Tag, Query, AggregateVariable, User, Group y Customer.

Y a modo de cierre, una tabla que recoge el papel de cada artefacto del esquema:

ArtefactoTipoPropósitoQuién lo consume
module_embeddings_<tipo>TablaAlmacén del vector + texto enviado a GeminiEl propio módulo (escritura y búsqueda tipada)
module_embeddings_<tipo>_contextVista lógicaRead Model con datos enriquecidos del recursoTextBuilder durante la generación
module_embeddings_unifiedVista lógicaUNION ALL con discriminador entity_typeBúsqueda cross-type cuando el agente no clasifica el tipo